package models;

import java.util.Calendar;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import javax.persistence.*;

import controllers.Events;
import controllers.Notifications;
import controllers.Users;

import net.sf.oval.constraint.MaxLength;
import notifiers.Mails;

import play.data.validation.Required;
import play.db.jpa.Model;

@Entity
@Table(name = "Tasks")
public class Task extends Model {

	@Column(name = "initial_effort_estimation")
	public double initialEffortEstimation;

	@Required
	public String priority;

	@Lob
	public String details;

	public String description;

	@Column(name = "creation_date")
	public Calendar creationDate;

	@Column(name = "resolving_date")
	public Calendar resolvingDate;

	@Column(name = "closing_date")
	public Calendar closingDate;

	public String note;

	@OneToMany(mappedBy = "task", cascade = { CascadeType.REMOVE })
	public List<DayTask> dayTasks = new LinkedList<DayTask>();

	@OneToMany(mappedBy = "task", cascade = { CascadeType.REMOVE })
	public List<TaskUserUser> taskUserAssignee = new LinkedList<TaskUserUser>();

	@OneToMany(mappedBy = "task", cascade = { CascadeType.REMOVE })
	public List<TaskUser> taskReviewers = new LinkedList<TaskUser>();

	@ManyToOne
	@JoinTable(name = "Tasks_User_Stories", joinColumns = @JoinColumn(name = "task_id"), inverseJoinColumns = @JoinColumn(name = "story_id"))
	public UserStory userStory;

	@Required
	@ManyToOne
	public TaskType type;

	@Required
	@ManyToOne
	public TaskStatus status;

	@ManyToMany
	public List<Task> dependOn = new LinkedList<Task>(); // list of tasks that a
	// task depends on

	@ManyToMany(mappedBy = "dependOn")
	public List<Task> dependants = new LinkedList<Task>(); // list of tasks that
	// depend on a task

	@ManyToOne
	public User creator;

	@ManyToMany(mappedBy = "tasks")
	public List<Event> events = new LinkedList<Event>();

	/**
	 * Constructs a new Task
	 * 
	 * @param description
	 * @param priority
	 * @param type
	 * @param status
	 * @param dependOn
	 * @param creator
	 */
	public Task(String description, String priority, TaskType type,
			TaskStatus status, List<Task> dependOn, User creator) {

		this.description = description;
		this.creationDate = Calendar.getInstance();
		this.priority = priority;
		this.type = type;
		this.status = status;
		this.creator = creator;
		this.dependOn = dependOn;

	}

	/**
	 * 
	 * @param summary
	 * @param description
	 * @param priority
	 * @param type
	 * @param status
	 * @param dependOn
	 * @param creator
	 */
	public Task(String description, String details, String priority,
			TaskType type, TaskStatus status, List<Task> dependOn, User creator) {
		this.details = details;
		this.description = description;
		this.creationDate = Calendar.getInstance();
		this.priority = priority;
		this.type = type;
		this.status = status;
		this.creator = creator;
		this.dependOn = dependOn;

	}

	/**
	 * constructs a new Task
	 * 
	 * @param description
	 * @param priority
	 * @param type
	 * @param status
	 * @param dependOn
	 * @param creator
	 * @param story
	 */
	public Task(String description, String priority, TaskType type,
			TaskStatus status, List<Task> dependOn, User creator,
			UserStory story) {

		this.description = description;
		this.creationDate = Calendar.getInstance();
		this.priority = priority;
		this.type = type;
		this.status = status;
		this.creator = creator;
		this.dependOn = dependOn;
		this.userStory = story;

	}

	public Task(String description, String details, String priority,
			TaskType type, TaskStatus status, List<Task> dependOn,
			User creator, UserStory story) {
		this.details = details;
		this.description = description;
		this.creationDate = Calendar.getInstance();
		this.priority = priority;
		this.type = type;
		this.status = status;
		this.creator = creator;
		this.dependOn = dependOn;
		this.userStory = story;

	}

	/**
	 * constructs a new Task
	 */
	public Task() {
		this.creationDate = Calendar.getInstance();

	}

	/**
	 * Returns the project of this task
	 * 
	 * @return the project of this task
	 */
	public Project getProject() {
		return events.get(0).project;
	}

	public Task delete() {
		for (Event event : events) {
			event.tasks.remove(this);
			event.save();
		}
		for (Task task : dependants) {
			task.dependOn.remove(this);
			task.save();
		}
		if (userStory != null)
			userStory.tasks.remove(this);
		if (status != null)
			status.tasks.remove(this);
		if (type != null)
			type.tasks.remove(this);
		return super.delete();
	}

	/**
	 * This method is used to create a new Task Object through the model and
	 * save it in the database.
	 * 
	 * @param description
	 * @param priority
	 * @param typeId
	 * @param statusId
	 * @param dependOn
	 * @param eventId
	 */
	public static void createTask(String description, String details,
			String priority, long typeId, long statusId, Long[] dependOn,
			long[] eventsId, long projectId, long storyId, long sprintId,
			long userId) {

		UserStory story = UserStory.findById(storyId);
		User creator = User.findById(userId);
		TaskType type = TaskType.findById(typeId);
		List<Task> dependsOnTasks = new LinkedList<Task>();
		TaskStatus status = TaskStatus.findById(statusId);
		if (dependOn != null)
			for (int i = 0; i < dependOn.length; i++) {
				Task d = Task.findById(dependOn[i]);
				dependsOnTasks.add(d);
			}
		Task task = new Task(description, details, priority, type, status,
				dependsOnTasks, creator);
		if (story != null)
			task.userStory = story;
		if (status.status.equals("STARTED"))
			task.resolvingDate = Calendar.getInstance();
		task.save();
		task.associateToEvents(eventsId, sprintId, projectId);
		new Log(1L, (Long) task.getEntityId(), "Task", "created the task",
				projectId, " ").save();

	}

	/**
	 * given a list of event ids and a sprint id this method checks if both are
	 * null and adds the task to the default else the task is associated to the
	 * corresponding events or sprint
	 * 
	 * @author Rana Ashraf
	 * @param eventsId
	 * @param sprintId
	 * @param projectId
	 */
	public void associateToEvents(long[] eventsId, long sprintId, long projectId) {

		Project project = Project.findById(projectId);
		if (eventsId != null && eventsId.length != 0) {
			for (int i = 0; i < eventsId.length; i++) {
				Event e = Event.findById(eventsId[i]);
				if (this.userStory != null) {
					if (e != null && !this.userStory.events.contains(e)) {
						Events.associateTask(e, this);
						e.save();
					}
				} else {

					if (e != null) {
						Events.associateTask(e, this);
						e.save();
					}
				}
			}
		}
		if (sprintId != 0L) {
			Event sprint = Event.findById(sprintId);
			Events.associateTask(sprint, this);
			sprint.save();
		}
		if (sprintId == 0L
				&& (eventsId == null || eventsId.length == 0 || this.events
						.isEmpty())) {
			Event defaultEvent = Events.getDefaultEvent(project);
			if (defaultEvent != null && !this.events.contains(defaultEvent)) {
				defaultEvent.tasks.add(this);
				defaultEvent.save();
			}
		}

	}

	/**
	 * edits a specific task as it connects with the model and changes the
	 * values of the attributes to the new input values and a mail is sent to
	 * the creator of the task in case he is not the one who edited the task.
	 * returns true if the task tyoe was changed to impediment and false
	 * otherwise
	 * 
	 * @author Rana Ashraf
	 * 
	 * @param description
	 * @param priority
	 * @param typeId
	 * @param statusId
	 * @param dependOn
	 * @param taskId
	 * 
	 */
	public static void editTask(String description, String details,
			String priority, long typeId, long statusId, long[] dependOn,
			long taskId, long projectId, long storyId, long[] eventsId,
			long sprintId, long userId, String userComment) {

		Project project = Project.findById(projectId);
		UserStory story = UserStory.findById(storyId);
		Task task = Task.findById(taskId);
		List<Task> dependsOnTasks = new LinkedList<Task>();
		List<Event> events = new LinkedList<Event>();
		TaskType type = TaskType.findById(typeId);
		TaskStatus status = TaskStatus.findById(statusId);
		Event defaultEvent = Events.getDefaultEvent(project);

		if (dependOn != null && dependOn.length != 0) {
			for (int i = 0; i < dependOn.length; i++) {
				Task d = Task.findById(dependOn[i]);
				if (d != null)
					dependsOnTasks.add(d);
			}
		}
		if (eventsId != null && eventsId.length != 0) {
			for (int i = 0; i < eventsId.length; i++) {
				Event event = Event.findById(eventsId[i]);
				if (event != null)
					events.add(event);
			}
		}
		if (task.events.contains(defaultEvent))
			events.add(defaultEvent);
		if (sprintId != 0L) {
			events.add((Event) Event.findById(sprintId));
		}

		if (status.status.equals("STARTED"))
			task.resolvingDate = Calendar.getInstance();
		else if (status.status.equals("CLOSED")) {
			task.closingDate = Calendar.getInstance();

		}
		task.disassociateTaskEvents();
		task.description = description;
		task.priority = priority;
		task.type = type;
		task.status = status;
		task.dependOn = dependsOnTasks;
		task.userStory = story;
		task.details = details;
		task.save();
		if (story != null)
			story.checkStoryStatus((Long) task.status.getEntityId(),
					(Long) task.getEntityId());
		task.associateToEvents(eventsId, sprintId, projectId);
		User editor = null;
		if (userId != (Long) task.creator.getEntityId()) {
			editor = User.findById(userId);

			if (editor != null && !type.type.equals("Impediment")) {
				new Notification((Long) task.creator.getEntityId(),
						"Task Edit", editor.firstName + " " + editor.lastName
								+ " has edited the task " + task.description)
						.save();

			}

		}
		new Log(userId, (Long) task.getEntityId(), "Task", "edited the task",
				projectId, " ").save();

	}

	/**
	 * disassociates a task from all the events it was associated with
	 * 
	 * @author Rana Ashraf
	 */
	public void disassociateTaskEvents() {
		if (this.events != null && !this.events.isEmpty())
			for (int i = 0; i < this.events.size();) {
				Event event = this.events.get(i);
				if (event.removeTask(this))
					event.save();
				else
					i++;
			}
	}

	/**
	 * returns the user assigned to a specific task. If no user is assigned to
	 * that task the method returns null
	 * 
	 * @author Rana Ashraf
	 * @return User
	 */
	public User getTaskAssignee() {
		User assignee = null;
		Long taskUserUserId = TaskUserUser
				.find(
						"select tuu.id from TaskUserUser tuu where tuu.task=? and tuu.status=?",
						this, true).first();
		if (taskUserUserId != null) {
			TaskUserUser tuu = TaskUserUser.findById(taskUserUserId);
			assignee = tuu.assignee;
		}
		return assignee;
	}

	/**
	 * Returns a list of all events associated to this task
	 * 
	 * @return a list of all events associated to this task
	 */
	public List<Event> getTaskEvents() {
		Set<Event> events = new HashSet<Event>();
		events.addAll(this.events);
		if (this.userStory != null)
			events.addAll(this.userStory.events);
		List<Event> result = new LinkedList<Event>();
		result.addAll(events);
		Event defaultEvent = Events.getDefaultEvent(this.events.get(0).project);
		result.remove(defaultEvent);
		return result;
	}

	/**
	 * This method returns a list of all tasks that a specific task is allowed
	 * to depend on. The list returned includes all tasks in a specific project
	 * excluding the tasks which depend on a specific task, and the task itself.
	 * 
	 * @author Rana Ashraf
	 * @param projectId
	 * 
	 *            return List<Task>
	 */
	public List<Task> nonDependantTasks(Long projectId) {
		Project project = Project.findById(projectId);
		List<Task> allTasks = project.getAllProjectTasks();
		List<Task> dependancyTask = this.dependants;
		dependancyTask.addAll(this.dependOn);
		dependancyTask.add(this);
		List<Task> output = new LinkedList<Task>();
		for (int i = 0; i < allTasks.size(); i++) {
			if (!dependancyTask.contains(allTasks.get(i)))
				output.add(allTasks.get(i));
		}

		return output;

	}

	/**
	 * Given the taskId, this method returns the component where the task with
	 * this taskId belong
	 * 
	 * @return Component
	 */
	public Component getTaskComponent() {
		Project p = null;
		User assignee = this.getTaskAssignee();

		if (this.userStory != null)
			return this.userStory.component;
		if (assignee != null) {
			p = this.events.get(0).project;

			return Users.getUserComponent((Long) assignee.getEntityId(),
					(Long) p.getEntityId());
		}
		return null;
	}

	/**
	 * returns the user reviewing a task with the status attribute equal true.
	 * returns null otherwise
	 * 
	 * @author Rana Ashraf
	 * @param status
	 * @return User
	 */
	public User getTaskReviewer(boolean status) {
		User reviewer = null;
		TaskUser tu = TaskUser.find(
				"select tu from TaskUser tu where tu.task=? and tu.status =?",
				this, status).first();

		if (tu != null)
			reviewer = tu.reviewer;
		return reviewer;
	}

	/**
	 * creates a new model object TaskUser with status = false which indicates
	 * that the user has a pending request to review a task
	 * 
	 * @author Rana Ashraf
	 * @param userId
	 * @return List<User>
	 */
	public List<User> requestReviewTask(long userId) {

		User reviewer = User.findById(userId);
		Project project = this.events.get(0).project;
		TaskUser tu = new TaskUser(reviewer, this).save();
		List<User> users = Users.getUsersWithPermission(
				"approve requests by developers to be reviewers",
				(Long) project.getEntityId());
		if (users != null) {
			users.remove(reviewer);
		}

		new Log(userId, (Long) tu.getEntityId(), "TaskUser",
				"requested to review the task", (Long) project.getEntityId(),
				" ").save();

		return users;
	}

	/**
	 * returns a list of records in Task User User Table that match with the
	 * given task ,the given assignee
	 * 
	 * @param u
	 *            User
	 * @return List<TaskUserUser>
	 */
	public List<TaskUserUser> getUserTaskRelation(User user) {

		List<TaskUserUser> TUU = TaskUserUser
				.find(
						"Select TUU from TaskUserUser TUU where TUU.task=? and TUU.assignee=? ",
						this, user).fetch();
		return TUU;

	}

	/**
	 * returns a list of records in Task User User Table that match with the
	 * given task
	 * 
	 * @return List<TaskUserUser>
	 */
	public List<TaskUserUser> getUserTaskRelation() {

		List<TaskUserUser> TUU = TaskUserUser.find(
				"Select TUU from TaskUserUser TUU where TUU.task=? ", this)
				.fetch();
		return TUU;

	}

	/**
	 * returns a list of records in Task User User Table that match with the
	 * given task ,the given assignee ,and the one who assigned
	 * 
	 * @param user
	 * @param assignee
	 * @return List<TaskUserUser>
	 */
	public List<TaskUserUser> getUserTaskRelation(User user, User assignee) {

		List<TaskUserUser> TUU = TaskUserUser
				.find(
						"Select TUU from TaskUserUser TUU where TUU.task=? and TUU.assignee=? and TUU.user=?",
						this, user, assignee).fetch();
		return TUU;

	}

	/**
	 * Assigns the currently logged in user to a specific task when he is not
	 * the reviewer nor requested to review this task. Returns true when
	 * assignment completed, and false when he is not assigned
	 * 
	 * @param taskId
	 * 
	 * @return boolean
	 */
	public boolean assignLoggedUserToTask(long userId) {
		boolean canAssign = false;

		if (this.getTaskReviewer(true) == null
				&& this.getTaskReviewer(false) == null)
			canAssign = true;
		else if (this.getTaskReviewer(true) != null) {
			if ((Long) this.getTaskReviewer(true).getEntityId() != userId)
				canAssign = true;
		} else if (this.getTaskReviewer(false) != null)
			if ((Long) this.getTaskReviewer(false).getEntityId() != userId)
				canAssign = true;

		if (canAssign) {
			User u = User.findById(userId);

			TaskUserUser TUU = new TaskUserUser(u, this);
			TUU.save();
			return true;
		} else

			return false;

	}

	/**
	 * returns a list of reviewers of a type which is the same as the type of a
	 * given task. The list returned contain reviewers in the same component as
	 * the logged in user in the given project.
	 * 
	 * @param projectId
	 * @param taskId
	 * @param userId
	 * 
	 * @return List<User>
	 */
	public List<User> getProjectReviewers(long projectId, long userId) {
		long assigneeId = -1;
		if (this.getTaskAssignee() != null)
			assigneeId = (Long) this.getTaskAssignee().getEntityId();
		Component component = Users.getUserComponent(userId, projectId);
		List<ComponentTaskTypeUser> users = component.taskTypeReviewers;

		List<User> reviewers = new LinkedList<User>();

		if (users != null) {
			int i = 0;
			while (i < users.size()) {
				if (!users.get(i).taskType.type.equals(this.type.type))
					users.remove(users.get(i));
				else
					i++;
			}

			for (ComponentTaskTypeUser u : users) {
				if (assigneeId == -1)
					reviewers.add(u.reviewer);
				else if ((Long) u.reviewer.getEntityId() != assigneeId)
					reviewers.add(u.reviewer);
			}
		}

		return reviewers;
	}

	/**
	 * assigns a user to be a reviewer of a specific task. Used when a user has
	 * the privellage to assign himself directly as a reviewer
	 * 
	 * @param userId
	 * @return boolean
	 */
	public boolean reviewTask(long reviewerId, long projectId) {

		User reviewer = User.findById(reviewerId);
		TaskUser review = new TaskUser(reviewer, this, true);
		review.save();
		this.taskReviewers.add(review);
		this.save();
		reviewer.taskReviewers.add(review);
		reviewer.save();
		new Log((Long) review.getEntityId(), "TaskUser", "\""
				+ reviewer.userName
				+ "\" is now assigned to review the task: \""
				+ this.description + "\"", projectId, " ").save();

		return true;
	}

	/**
	 * Assigns a certain reviewer to a specific task in a specific project and
	 * notifies the reviewer that his request has been accepted.
	 * 
	 * @param projectId
	 * @param taskId
	 * @param reviewerId
	 * 
	 * @return boolean
	 */
	public boolean assignReviewer(long reviewerId, long projectId) {
		User reviewer = User.findById(reviewerId);
		this.reviewTask(reviewerId, projectId);
		String subject = "Task Review";

		new Notification((Long) reviewer.getEntityId(), subject,
				"You have been assigned to review a task.").save();
		return true;
	}

	/**
	 * edit or insert the estimation effort for a certain task
	 * 
	 * @param taskID
	 *            the task id
	 *@param effort
	 *            the estimation effort
	 */
	public String fillTotalEffort(double effort) {
		String message;

		boolean hasUserStory;

		if (this.userStory == null) {
			hasUserStory = false;
			message = "estimation inserted successfully";
			this.initialEffortEstimation = effort;
		} else {
			hasUserStory = true;
			UserStory taskStory = this.userStory;
			double usEstimation = taskStory.effortEstimation;
			double taskStoryTasksTotalEffort = 0;
			List<Task> taskStoryTasks = taskStory.tasks;
			for (int i = 0; i < taskStoryTasks.size(); i++)
				taskStoryTasksTotalEffort = taskStoryTasks.get(i).initialEffortEstimation
						+ taskStoryTasksTotalEffort;

			if (effort < usEstimation - taskStoryTasksTotalEffort) {
				this.initialEffortEstimation = effort;
				message = "estimation inserted successfully";
			} else {
				message = "this estimation can not be inserted since the total effort for all tasks in the users story will exceeds the  estimation effort for the user story";
			}
		}

		this.save();
		return message;

	}

	public void assignDeveloperTask(long userId, long assignerId) {
		User user = User.findById(userId);
		User assigner = User.findById(assignerId);
		TaskUserUser TUU = new TaskUserUser(user, this);
		TUU.save();
		this.deleteSuggestedTasks();
	}

	/**
	 * deletes all suggests to this task from any user
	 * 
	 * @param t
	 *            Task
	 */
	private void deleteSuggestedTasks() {
		List<TaskUserUser> TUU = this.getUserTaskRelation();
		for (int i = 0; i < TUU.size(); i++) {

			if (TUU.get(i).user != null) {
				TUU.get(i).delete();
			}

		}
	}

	public void suggestDeveloperToTask(long userId, long suggesterId) {
		User user = User.findById(userId);
		User suggester = User.findById(suggesterId);
		TaskUserUser TUU = new TaskUserUser(suggester, user, this);

		TUU.save();

	}

	public void rejectSuggestedTask(long userId) {
		User user = User.findById(userId);

		List<TaskUserUser> TUU = this.getUserTaskRelation(user);
		for (int i = 0; i < TUU.size(); i++) {
			TUU.get(i).delete();

		}
	}
}
